Skip to content

fix(ios): guard delayed manual keyboard did events by current keyboard state#1338

Open
litinskii wants to merge 1 commit intokirillzyusko:mainfrom
litinskii:fix-ios-manual-did-events-race
Open

fix(ios): guard delayed manual keyboard did events by current keyboard state#1338
litinskii wants to merge 1 commit intokirillzyusko:mainfrom
litinskii:fix-ios-manual-did-events-race

Conversation

@litinskii
Copy link
Copy Markdown
Contributor

📜 Description

Fix race condition in manual iOS did keyboard events introduced in #1161.

When navigating between screens, keyboard events may arrive in non-linear order (willHide -> didShow -> didHide). The last delayed didHide could incorrectly win and reset keyboard height to 0, even when keyboard is already visible on the new screen.

This change keeps the manual did strategy from #1161, but validates actual keyboard state at delayed execution time.

💡 Motivation and Context

After upgrading to 1.20.6, some navigation flows started producing incorrect keyboard offset on the next screen:

  • keyboard is visible
  • but computed offset is 0
  • content goes under keyboard

Root cause: delayed DispatchWorkItem tasks may execute out of logical order during responder/navigation transitions.

This PR prevents stale delayed events from overriding current keyboard state.

Problem Statement (clear event order issue)

When navigating between screens, keyboard events can arrive in an invalid order:

  1. keyboard hide
  2. keyboard show end
  3. keyboard hide end

As a result, the last processed event is hide, keyboard height is set to 0, and offset is recalculated to 0.
On the next screen, keyboard is visible, but bottom inset is not applied, so content goes under the keyboard.

Reproduction

Keyboard.dismiss();

await wait(400);

// navigate to screen with auto-focus input
navigation.navigate(...);

Expected:

  • final keyboard state should match visible keyboard (height > 0)
  • bottom offset should remain applied on the destination screen

Actual before fix:

  • delayed stale didHide may run last
  • keyboard height/offset becomes 0 while keyboard is still visible

📢 Changelog

iOS

  • validate delayed did event against current keyboard visibility before emitting didShow/didHide
  • add stale-task guard (keyboardDidEventID) so old delayed tasks cannot override newer state
  • cancel pending delayed did tasks and clean up watchers/KVO on unmount
  • keep manual did dispatch approach from fix: manual did events #1161 (no rollback to system keyboardDid*)

🤔 How Has This Been Tested?

Tested manually on iOS navigation scenario with focused input:

  1. Open screen A with focused input (keyboard visible).
  2. Navigate quickly to screen B where input becomes focused.
  3. Reproduce event interleaving where hide/show callbacks race.
  4. Verify final state:
    • keyboard remains visible
    • keyboardDidHide does not incorrectly win
    • keyboard height/offset is not reset to 0
    • content stays above keyboard on the new screen

Also verified unmount path does not emit late delayed did events from removed screen observer.

📸 Screenshots (if appropriate):

Before

2026-03-04.11.18.57.mov

After

2026-03-04.10.58.30.mov

📝 Checklist

  • CI successfully passed
  • I added new mocks and corresponding unit-tests if library API was changed

@kirillzyusko
Copy link
Copy Markdown
Owner

Hey @litinskii

Is this problem still actual even for 1.21.0-beta.3? Shouldn't didShow event simply cancel previous willHide schedule?

@kirillzyusko kirillzyusko self-requested a review March 4, 2026 10:18
@kirillzyusko kirillzyusko self-assigned this Mar 4, 2026
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🍎 iOS iOS specific labels Mar 4, 2026
@litinskii
Copy link
Copy Markdown
Contributor Author

1.20.7 with bug

2026-03-04.11.18.57.mov

1.20.5

2026-03-04.17.10.31.mov

1.20.7 with this PR

2026-03-04.10.58.30.mov

1.21.0-beta.3 is it bug or it just start working in another way

2026-03-04.17.18.32.mov

Thanks, that makes sense. I tested 1.21.0-beta.3 from npm (not from a local git tag/branch), so I can’t point to an exact repo SHA from my local clone.

This can still happen in 1.21.0-beta.3 (in another way i suppose) in navigation/auto-focus flows, because didShow can only cancel willHide if both schedules belong to the same active observer instance and lifecycle window.

In this race, the delayed hide is often scheduled by the previous screen observer, then the next screen mounts and schedules show. Those tasks can complete out of order, and a stale didHide may still run last after transition, resetting height/offset to 0 while keyboard is visible.

So “didShow cancels previous willHide” is necessary, but not sufficient for this case.
In this PR I try to validates actual keyboard visibility at execution time and cancels pending tasks on unmount.

PR is focused on the stale delayed-event race during navigation/auto-focus:
hide -> showEnd -> hideEnd, where a stale didHide can still win and reset height/offset to 0 while keyboard is visible.

So “didShow cancels previous willHide” is helpful, but not always sufficient across observer lifecycle boundaries (previous screen vs next screen).

Could you share the exact commit SHA used for 1.21.0-beta.3? I don’t see that tag/branch in my local clone.

This is the smallest fix I found for this specific race, but I agree there may be a better long-term design.
I’m happy to rework the PR if you suggest a cleaner approach.

@kirillzyusko
Copy link
Copy Markdown
Owner

Could you share the exact commit SHA used for 1.21.0-beta.3?

Sure, this is commit: 11c0dbf

I don’t see that tag/branch in my local clone.

Yes, because I forgot to create them 😬

Thank you for videos! It helps a lot! I'll try to analyze problem in more details (I had only a quick look). Thank you again for raising this issue and preparing solution ❤️

@kirillzyusko
Copy link
Copy Markdown
Owner

BTW @litinskii Don't you mind to switch to discord (nickname is kiryl.ziusko) for further communication?

I think communication in messenger will be much faster and more productive 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working 🍎 iOS iOS specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants